Back

Radial Cross-Section Generator

This code prepares an axial slice of a radially symmetric slice from a 2D z-pinch for use in PERSEUS simulations. It's supplementary to read_vtk.py by James Young (GitHub).

This code is too long to fix all of my stupid comments and odd function names, so I will just leave them in. I hope they are as amusing to you as they are to me.

from read_vtk import *
from read_vtk import mesh
import cv2 
from scipy.interpolate import CubicSpline, interp1d
import matplotlib.pyplot as plt

####################
# DOMAIN VARIABLES #
####################
"""DEPENDENT ON YOUR SIMULATION'S (PERSEUS) PARAMETERS"""
ngu = 2  # number of ghost cells (for PERSEUS, this is 2)
nx = 161  # number of cells in the x direction
nz = 545  # number of cells in the z direction
wavelength = 532e-9  # wavelength of the laser (in meters)
rlength_PERSEUS = 1e-3  # length (in r) of the simulation
zlength_PERSEUS = 3.375e-3  # length (in z) of the simulation

"""INDEPENDENT DOMAIN VARIABLES"""
ntiles = 64  # for splitting the 2d domain into tiles
ntilesrow = 8
target_resolution = wavelength/30  # Should be approx. λ/30 for laser simulation
dl = target_resolution  # second definition of the same variable, because I cannot decide what to name it.

"""DEPENDENT DOMAIN VARIABLES"""
dl_PERSEUS = rlength_PERSEUS/nx
scale_factor = dl_PERSEUS/dl
slice_index = int(.5*nz) #the index of the slice you want to analyze (in the z direction)
"""SWITCH VARIABLES"""
plot_x_pinch = False
plot_tiles = False
plot_pizza = False
plot_rh_i = False #plot the original z-pinch
print_boundary_corner_coordinates = True #prints x1,x2,y1,y2 for the upsampled window
plot_boundary_box_location = False #creates a fake upsampled pizza then plots a box connecting the 4 corners of the boundary box so you have an idea of where it is with regards to the entire cross section slice
convert_to_dat_file = True #determines if the tiles get saved as dat files at the end
plot_pizzas = False
plot_upsampled_pizzas = False
convert_to_dat_file = True


########################
# FUNCTION DEFINITIONS #
########################
def get_field_value(x,y,slice_array): #gets the value of a field at a given radial distance from the center of the slice
    x = np.float64(x)
    y = np.float64(y)
    rval = np.sqrt(x**2 + y**2)
    rval = int(rval)
    if rval >= len(slice_array):
        value = np.min(slice_array)
    else:
        value = slice_array[rval]
    return value


def circle_pizza(target_field): #creates a 2D representation of a radially symmetric slice of a given target field
    smesh = unstructured_to_structured(mesh, target_field)
    nx, nz, nu = smesh.dimensions
    print(nx,nz)
    Fy = get_mesh_subset(smesh, [0,nx], [0,nz], target_field)
    print(np.shape(Fy))
    slice_array = Fy[slice_index,:]
    #slice_array = np.log(slice_array)/np.log(10) #CC comment this line out to turn off log of ion density
    x = np.arange(-nx,nx,1)
    y = np.arange(-nx,nx,1)
    r = np.zeros((len(x)-1,len(y)-1))
    for i in range(len(x)-1):
        for j in range(len(y)-1):
            r[i,j] = get_field_value(x[i],y[j],slice_array)
    return r

def bring_your_own_pizza(slice_array, *boundaries,windowed): #generates a 2D (radially symmetric) array based on the given slice array (1D array where slice[0] is the center of the 2d disc))
    if windowed == True:
        x1 = boundaries[0]
        x2 = boundaries[1]
        y1 = boundaries[2]
        y2 = boundaries[3]
        window_pizza = np.zeros([x2-x1,y2-y1])
        for i in range(x2-x1):
            for j in range(y2-y1):
                windowed-slice
                window_pizza[i,j] = get_field_value(x1+i,y2+j,upsampled_slice)        
        return window_pizza    
    else:
        x = np.arange(-len(slice_array),len(slice_array),1)
        y = np.arange(-len(slice_array),len(slice_array),1)
        r = np.zeros((len(x)-1,len(y)-1))
        for i in range(len(x)-1):
            for j in range(len(y)-1):
                r[i,j] = get_field_value(x[i],y[j],slice_array)
    return r

def linear_upsample(slice_array, scale_factor):
    original_indices = np.arange(len(slice_array))
    new_length = int(len(slice_array) * scale_factor)
    new_indices = np.linspace(0, len(slice_array) - 1, new_length)
    interp_func = interp1d(original_indices, slice_array, kind='linear', fill_value="extrapolate")
    upsampled_slice = interp_func(new_indices)
    return upsampled_slice

def colorbar_plot(target_field,target_array,vmin=None,vmax=None):
    plt.title(f"{target_field} at z_index={slice_index} & t={time_analyze}")
    plt.imshow(target_array,vmin=vmin,vmax=vmax)
    plt.colorbar()
    plt.show()

def sheet_pizza(array, nx, nz): #splits the 2D array into smaller tiles (for MPI purposes)
    ntilesrow = int(np.sqrt(ntiles))  # We want a 4x4 grid
    tile_size_x = int(nx / ntilesrow)  # Size of each tile in x dimension
    tile_size_z = int(nz / ntilesrow)  # Size of each tile in z dimension
        
    tile_array = np.ones((tile_size_x, tile_size_z, ntiles))

    tile_num = 0
    for m in range(ntilesrow):
        for n in range(ntilesrow):
            x_start = m * tile_size_x
            x_end = (m + 1) * tile_size_x
            z_start = n * tile_size_z
            z_end = (n + 1) * tile_size_z

            tile_array[:, :, tile_num] = array[x_start:x_end, z_start:z_end]

            tile_num += 1
    return tile_array

def cell_subplots(tile_array,ntilesrow,vmin=None,vmax=None): #creates a grid of subplots representing each tile in tile_array
    fig,axes = plt.subplots(ntilesrow, ntilesrow, figsize=(10, 10))
    for i in range(ntiles):
        target_row = i // ntilesrow
        target_col = i % ntilesrow

        axes[target_row, target_col].imshow(tile_array[:, :, i], cmap='viridis',vmin=vmin,vmax=vmax)
    plt.tight_layout()
    plt.show()
    return

def fill_gu(current_cell, target_cell, direction): #supplementary function for "ghost_cell" function
    if direction == "above":
        current_cell[ngu:-ngu, :ngu] = target_cell[ngu:-ngu, -2*ngu:-ngu]
    if direction == "below":
        current_cell[ngu:-ngu, -ngu:] = target_cell[ngu:-ngu, ngu:2*ngu]
    if direction == "right":
        current_cell[-ngu:, ngu:-ngu] = target_cell[ngu:2*ngu, ngu:-ngu]
    if direction == "left":
        current_cell[:ngu, ngu:-ngu] = target_cell[-2*ngu:-ngu, ngu:-ngu]
    else:
        print("bad direction, try again")
    return current_cell

def ghost_cell(tile_array): #fills the ghost cells for all tiles in tile_array using values from neighboring tiles
    ntiles = len(tile_array[0, 0, :]) #look at how big the third dimension of tile array is (each xy slice is 1 tile, and there are "ntiles" of these slices in z)
    gu_array = np.zeros((len(tile_array[:, 0, 0]) + 2 * ngu, len(tile_array[:, 0, 0]) + 2 * ngu, ntiles)) #initializes an empty array that's larger than before (space for ghost cells)
    for tilenum in range(ntiles): #fill known values so zeros are only on the outer two cells of the arrays (these are the ghost cells)
        gu_array[ngu:-ngu, ngu:-ngu, tilenum] = tile_array[:, :, tilenum]  
    for tilenum in range(ntiles): #loops through all the tiles and performs checks so that the ghost cells are filled in appropriately depending on their edge characteristics
        if (tilenum % ntilesrow != 0):  # ∃ A TILE ABOVE THIS ONE
            gu_array[:, :, tilenum] = fill_gu(gu_array[:, :, tilenum], gu_array[:, :, tilenum - 1], "above")
        if (tilenum % ntilesrow != ntilesrow - 1):  # ∃ A TILE BELOW THIS ONE
            gu_array[:, :, tilenum] = fill_gu(gu_array[:, :, tilenum], gu_array[:, :, tilenum + 1], "below")
        if (tilenum - ntilesrow >= 0):  # ∃ A TILE TO THE LEFT OF THIS ONE
            gu_array[:, :, tilenum] = fill_gu(gu_array[:, :, tilenum], gu_array[:, :, tilenum - ntilesrow], "left")
        if (tilenum + ntilesrow < ntiles):  # ∃ A TILE TO THE RIGHT OF THIS ONE
            gu_array[:, :, tilenum] = fill_gu(gu_array[:, :, tilenum], gu_array[:, :, tilenum + ntilesrow], "right")
    return gu_array

def zpinch_to_pizza(slice_array): #creates a 2D representation of a radially symmetric slice of a given target field
    x = np.arange(-nx,nx,1)
    y = np.arange(-nx,nx,1)
    r = np.zeros((len(x)-1,len(y)-1))
    for i in range(len(x)-1):
        for j in range(len(y)-1):
            r[i,j] = get_field_value(x[i],y[j],slice_array)
    return r

def create_pizza_arrays():
    # Variable definitions
    str_vars = [ #list of variables to be processed (∃ a function that pulls this from the vtk file. I know this because I probably wrote it at some point. but is it in this code? no. no it is not. But this explicit list was good enough for me at the time)
        'Ion Density', 'Current Density', 'Electric Field', 'Electron Density',
        'Electron Temperature', 'Electron Velocity', 'Grad Pe', 'Ion Temperature',
        'Ion Velocity', 'Magnetic Field', 'eta x-direction', 'eta y-direction', 'eta z-direction'
    ]
    array = np.zeros((nz, 83, len(str_vars)*3))
    pizza_array = np.zeros((nx*2-1, nx*2-1, len(str_vars)*3))
    str_vars_w_component = []
    counter = 0

    for var in str_vars:
        print(f"Processing: {var} (counter={counter})")
        ndimensions, *components = string_to_array(var)
        array[:,:,counter:counter+3] = np.stack(components, axis=2)

        if ndimensions == 1: #if the variable is a scalar
            slice_array = array[int(nz/2),:,counter]
            pizza_array[:,:,counter] = zpinch_to_pizza(slice_array)
            str_vars_w_component.append(f"{var}{counter}")
            counter += 1
        elif ndimensions == 3: #if the variable is a vector
            for i, component in enumerate(['x', 'y', 'z']):
                slice_array = array[int(nz/2),:,counter+i]
                pizza_array[:,:,counter+i] = zpinch_to_pizza(slice_array)
                str_vars_w_component.append(f"{var}_{component}{counter+i}")
            counter += 3

    print("Components created:", str_vars_w_component)
    return array, pizza_array, str_vars_w_component

array, pizza_array, str_vars_w_component = create_pizza_arrays()

##########################
# RECOVER Qin VARIABLES #
##########################
Qin_vars = ['rh','mx','my','mz','en','bx','by','bz','ex','ey','ez',
            'jx','jy','jz','et','ne','ep','etax','dpx','dpy','etay','etaz']

Qin_pizza_array = np.zeros((len(pizza_array[:,0,0]), len(pizza_array[0,:,0]), len(Qin_vars)))
"""REASSIGNING VARIABLES TO BE DIMENSIONLESS"""
Qin_pizza_array[:,:,0] = pizza_array[:,:,0]/n0                  # rh (ion density)
Qin_pizza_array[:,:,15] = pizza_array[:,:,7]/n0                 # ne (electron density)
Qin_pizza_array[:,:,11:14] = pizza_array[:,:,1:4]/j0           # jx, jy, jz
Qin_pizza_array[:,:,8:11] = pizza_array[:,:,4:7]/e0            # ex, ey, ez
Qin_pizza_array[:,:,17] = pizza_array[:,:,22]/eta0             # etax
Qin_pizza_array[:,:,20:22] = pizza_array[:,:,23:25]/eta0       # etay, etaz
Qin_pizza_array[:,:,5:8] = pizza_array[:,:,19:22]/b0          # bx, by, bz
Qin_pizza_array[:,:,1:4] = pizza_array[:,:,16:19]*pizza_array[:,:,0]/n0/v0  # mx, my, mz
Qin_pizza_array[:,:,4] = pizza_array[:,:,15]*pizza_array[:,:,0]/n0/p0       # en (ion internal energy)
Qin_pizza_array[:,:,16] = pizza_array[:,:,8]*pizza_array[:,:,7]/te0         # ep (electron pressure)
Qin_pizza_array[:,:,14] = pizza_array[:,:,8]*pizza_array[:,:,7]/n0/p0       # et (electron total energy)
Qin_pizza_array[:,:,18:20] = pizza_array[:,:,12:14]*L0/p0                   # dpx, dpy

if plot_pizzas == True:
    n = len(array[0,0,:])
    nrows = int(np.ceil(np.sqrt(n)))
    ncols = int(np.ceil(n / nrows))
    
    fig, axes = plt.subplots(nrows, ncols, figsize=(15, 15))
    axes = axes.flatten()
    
    for i, var in enumerate(Qin_vars):
        axes[i].imshow(Qin_pizza_array[:,:,i])
        axes[i].set_title(var)
        axes[i].axis('off')
    
    # Remove empty subplots
    for j in range(i+1, len(axes)):
        fig.delaxes(axes[j])
    
    plt.tight_layout()
    plt.show()

for i in range(len(Qin_pizza_array[0,0,:])): #loop through all the variables and save them as dat files
    slice = Qin_pizza_array[nx-1:2*nx-1,nx-1,i]
    upsampled_slice = linear_upsample(slice, scale_factor)
    
    # Initialize arrays on first iteration
    if i == 0:
        nx_us = len(upsampled_slice)
        dl_us = rlength_PERSEUS/nx_us
        x1 = int(x_chosen-(32*wavelength/dl_us))
        x2 = int(x_chosen+(32*wavelength/dl_us))
        y1 = int(y_chosen-(32*wavelength/dl_us))
        y2 = int(y_chosen+(32*wavelength/dl_us))
        boundaries = (x1, x2, y1, y2)
        upsampled_pizza = bring_your_own_pizza(upsampled_slice, *boundaries, windowed=True)
        tile_array = sheet_pizza(upsampled_pizza, np.shape(upsampled_pizza)[0], np.shape(upsampled_pizza)[1])
        array_with_gus = ghost_cell(tile_array)
    
    # Save tiles as dat files if requested
    if convert_to_dat_file:
        save_tiles_in_log_form = False
        for current_tile in range(ntiles):
            mpi_x = current_tile // ntilesrow
            mpi_y = current_tile % ntilesrow
            filename = f'radial_slice_tile_{mpi_x+1}_{mpi_y+1}_{Qin_vars[i]}.dat'
            
            dat_array = (np.log10(array_with_gus[:,:,current_tile]) if save_tiles_in_log_form 
                        else array_with_gus[:,:,current_tile])
            
            np.savetxt(f"{save_directory}{filename}", dat_array, fmt=f'%.{truncate_to}e')
            
            if (mpi_x == 7 and mpi_y == 7):
                print(f"Variable {Qin_vars[i]} Saved Successfully!")

if plot_upsampled_pizzas == True:
    n = len(Qin_vars)
    nrows = int(np.ceil(np.sqrt(n)))
    ncols = int(np.ceil(n / nrows))

    fig, axes = plt.subplots(nrows, ncols, figsize=(15, 15))
    axes = axes.flatten()

    counter = 0
    for i, var in enumerate(Qin_vars):
        axes[i].imshow(pizza_array[:,:,counter])
        axes[i].set_title(var)
        axes[i].axis('off')
        counter += 1

    # Remove empty subplots if any
    for j in range(i+1, len(axes)):
        fig.delaxes(axes[j])

    plt.tight_layout()
    plt.show()

Documentation (HTML) formatted with assistance from Anthropic's Claude, an LLM